Skip to content

Conversation

mp911de
Copy link
Member

@mp911de mp911de commented Aug 29, 2025

We now provide AOT support to generate repository implementations during build-time for Cassandra repository query methods.

Supported Features

  • Derived query methods, @Query and named query methods.
  • Window, Slice, Stream, and Optional return types
  • Sort query rewriting
  • Interface and DTO Projections
  • Value Expressions (Those require a bit of reflective information.
    Mind that using Value Expressions requires expression parsing and contextual information to evaluate the expression)

Limitation

  • Vector Search not yet supported

Excluded methods

  • CrudRepository and other base interface methods as their implementation is provided by the base class respective fragments
  • Vector Search Methods

Example repository:

interface PersonRepository extends CrudRepository<Person, String> {

	@Query(idempotent = Query.Idempotency.IDEMPOTENT)
	Person findByFirstname(String firstname);

	Stream<PersonDto> streamDtoProjectionByFirstname(String firstname);

	@Query("select numberOfChildren from person where firstname = :firstname")
	int findDeclaredNumberOfChildrenByFirstname(String firstname);

	Window<Person> findWindowByLastname(String lastname, ScrollPosition scrollPosition, Limit limit);
}

Generated fragment:

/**
 * AOT generated Cassandra repository implementation for {@link PersonRepository}.
 */
public class PersonRepositoryImpl__AotRepository extends AotRepositoryFragmentSupport {
  private final CassandraOperations operations;

  public PersonRepositoryImpl__AotRepository(CassandraOperations operations,
      RepositoryFactoryBeanSupport.FragmentCreationContext context) {
    super(operations, context);
    this.operations = operations;
  }

  /**
   * AOT generated implementation of {@link PersonRepository#findByFirstname(java.lang.String)}.
   */
  public Person findByFirstname(String firstname) {
    Query query = Query.query(Criteria.where("firstname").is(firstname));

    ExecutableSelectOperation.TerminatingSelect<Person> select = operations.query(Person.class).matching(query);
    return select.oneValue();
  }

  /**
   * AOT generated implementation of {@link PersonRepository#streamDtoProjectionByFirstname(java.lang.String)}.
   */
  public Stream<PersonRepository.PersonDto> streamDtoProjectionByFirstname(String firstname) {
    Query query = Query.query(Criteria.where("firstname").is(firstname));

    ExecutableSelectOperation.TerminatingSelect<PersonRepository.PersonDto> select = operations.query(Person.class).as(PersonRepository.PersonDto.class).matching(query);
    return select.stream();
  }

  /**
   * AOT generated implementation of {@link PersonRepository#findDeclaredNumberOfChildrenByFirstname(java.lang.String)}.
   */
  public int findDeclaredNumberOfChildrenByFirstname(String firstname) {
    Object[] args = new Object[1];
    args[0] = potentiallyConvertBindingValue(firstname);
    SimpleStatement query = SimpleStatement.newInstance("select numberOfChildren from person where firstname = ?", args);

    ExecutableSelectOperation.TerminatingResults<Integer> select = operations.query(query).as(Integer.class);
    return select.oneValue();
  }

  /**
   * AOT generated implementation of {@link PersonRepository#findWindowByLastname(java.lang.String, org.springframework.data.domain.ScrollPosition, org.springframework.data.domain.Limit)}.
   */
  public Window<Person> findWindowByLastname(String lastname, ScrollPosition scrollPosition,
      Limit limit) {
    Query query = Query.query(Criteria.where("lastname").is(lastname));
    if (limit.isLimited()) {
      query = query.limit(limit.max() + 1);
    }
    if (!scrollPosition.isInitial()) {
      query = query.pagingState((CassandraScrollPosition) scrollPosition);
    }
    QueryOptions.QueryOptionsBuilder optionsBuilder = QueryOptions.builder();
    if (limit.isLimited()) {
      optionsBuilder.pageSize(limit.max());
    }
    query = query.queryOptions(optionsBuilder.build());

    ExecutableSelectOperation.TerminatingSelect<Person> select = operations.query(Person.class).matching(query);
    return WindowUtil.of(select.slice());
  }
  
}

Metadata (truncated):

{
  "name": "org.springframework.data.cassandra.repository.aot.PersonRepository",
  "module": "Cassandra",
  "type": "IMPERATIVE",
  "methods": [
    {
      "name": "findByFirstname",
      "signature": "public abstract org.springframework.data.cassandra.domain.Person org.springframework.data.cassandra.repository.aot.PersonRepository.findByFirstname(java.lang.String)",
      "query": {
        "query": "SELECT * FROM person WHERE firstname=?"
      }
    },
    {
      "name": "streamDtoProjectionByFirstname",
      "signature": "public abstract java.util.stream.Stream<org.springframework.data.cassandra.repository.aot.PersonRepository$PersonDto> org.springframework.data.cassandra.repository.aot.PersonRepository.streamDtoProjectionByFirstname(java.lang.String)",
      "query": {
        "query": "SELECT * FROM person WHERE firstname=?"
      }
    },
    {
      "name": "findDeclaredNumberOfChildrenByFirstname",
      "signature": "public abstract int org.springframework.data.cassandra.repository.aot.PersonRepository.findDeclaredNumberOfChildrenByFirstname(java.lang.String)",
      "query": {
        "query": "select numberOfChildren from person where firstname = ?"
      }
    },
    {
      "name": "findWindowByLastname",
      "signature": "public abstract org.springframework.data.domain.Window<org.springframework.data.cassandra.domain.Person> org.springframework.data.cassandra.repository.aot.PersonRepository.findWindowByLastname(java.lang.String,org.springframework.data.domain.ScrollPosition,org.springframework.data.domain.Limit)",
      "query": {
        "query": "SELECT * FROM person WHERE lastname=?"
      }
    }
  ]
}

onobc and others added 12 commits August 26, 2025 13:58
The configuration/extension and entry point of CassandraRepositoryContributor
are in somewhat good shape but the latter breaks down when getting to the
"ok lets generated some code" stage. Namely, there are still large helpings
of "MongoDB-AOT-repo-copy-pasta" laying around and this is where I was
struggling to bridge the gap between Spring Data MongoDB and
Spring Data Cassandra,

Signed-off-by: Chris Bono <[email protected]>
Add TCK tests and fix remaining issues.
@mp911de mp911de added this to the 5.0 M6 (2025.1.0) milestone Aug 29, 2025
@mp911de mp911de added type: enhancement A general enhancement theme: aot An issue related to Ahead-Of-Time processing labels Aug 29, 2025
@mp911de mp911de linked an issue Aug 29, 2025 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme: aot An issue related to Ahead-Of-Time processing type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for Cassandra AOT Repositories
3 participants